#  This file is part of OpenLightingDesigner.
# 
#  OpenLightingDesigner is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  OpenLightingDesigner is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with OpenLightingDesigner.  If not, see <http://www.gnu.org/licenses/>.
#
# MainController.py
# Copyright (c) 2010 - 2011 Matthew Reid

import os, sys
import os.path
import time
import threading
#import jsonpickle

from Bases import BaseObject, OSCBaseObject, BaseThread, ChildGroup, Config, Archive, FileManager, Serialization
#from DeviceSystem.DeviceSystem import DeviceSystem
#from DeviceSystem import Groups
from commdispatcher import CommDispatcher
from ui.iOSCControl.iOSCController import iOSCController
import DeviceSystem
from modifiers.EffectsSystem import EffectsSystem
from modifiers.snapshot import Stack, Snapshot
from modifiers.playback import Playback

Groups = DeviceSystem.Groups


class MainController(OSCBaseObject):
    _saved_class_name = 'MainController'
    _saved_child_objects = ['Groups', 'CueStacks', 'Submasters']
    _saved_attributes = ['session_name', 'show_name', 'current_filename']
    #saved_child_classes = ['Stack']
    _Properties = {'current_filename':dict(type=str), 
                   'show_name':dict(default='')}
    _ChildGroups = {'CueStacks':dict(child_class=Stack, 
                                     send_child_updates_to_osc=True), 
                    'Groups':dict(child_class=Groups.DeviceGroup.DeviceGroup, 
                                  send_child_updates_to_osc=True), 
                    'Submasters':{}, 
                    'Playbacks':dict(child_class=Playback)}
    BuildEmissionThread = True
    def __init__(self, **kwargs):
        self.shutting_down = False
        self.comm = CommDispatcher()
        self.osc_address = 'Main'
        self.osc_parent_node = self.comm.osc_io.root_node
        self.osc_child_nodes = {}
        self.globals_filename = 'Globals.olg'
        self.init_globals = False
        self.dump_request_timer = None
        super(MainController, self).__init__(**kwargs)
        self.iOsc = iOSCController(comm=self.comm, osc_parent_node=self.osc_node)
        self.iOsc.MainController = self
        filetypes = [dict(extension='.ols', 
                          mimetype='application/x-openlightingdesigner-show', 
                          description='Show File', 
                          load_callback=self.load_show_file, 
                          save_callback=self.save_show_file),  
                     dict(extension='.olg', 
                          mimetype='application/x-openlightingdesigner-globals', 
                          description='Global Data', 
                          is_main_filetype=False, 
                          load_callback=self.load_globals_file, 
                          save_callback=self.save_globals_file)]
        self.FileManager = FileManager(filetype_data=filetypes)
        self.FileManager.bind(current_file=self.on_FileManager_current_file)
        self.Archive = Archive()
        self.loading_from_osc = False
        self.dump_request_thread = None
        self.add_osc_handler(callbacks={'request-dump':self.on_osc_request_dump})
        
        self.register_signal('new_group', 'remove_group', 'group_index_changed', 'app_exit_complete', 
                             'new_cuestack', 'new_submaster', 'playback_added', 'child_added')
            
        ds_kwargs = {'MainController':self}
        ds_kwargs.update(self.add_osc_child(address='Devices'))
        self.DeviceSystem = DeviceSystem.DeviceSystem(**ds_kwargs)
        self.DeviceSystem.connect('new_device', self.on_ds_new_device)
        for key in ['Groups', 'CueStacks']:
            self.Archive.add_member(name=key, 
                                    path='MainController', 
                                    filename='%s.js' % (key), 
                                    serialize_obj={'MainController':self}, 
                                    serialize_kwargs={'MainController':{'saved_child_objects':[key]}})
        es_kwargs = {'MainController':self}
        es_kwargs.update(self.add_osc_child(address='EffectsSystem'))
        self.EffectsSystem = EffectsSystem(**es_kwargs)

        self.load_globals()
        
        f = self.GLOBAL_CONFIG['arg_parse_dict'].get('filename')
        if f:
            t = threading.Timer(10.0, self.FileManager.load_file, kwargs={'filename':f})
            t.start()
            #self.load_file(filename=f)
        
        #self.comm.connect('state_changed', self.on_comm_state)
        #self.comm.osc_io.connect('new_master', self.on_osc_new_master)
        self.comm.osc_io.SessionManager.bind(oscMaster=self.on_osc_new_master)
        self.comm.MainController = self
        self.comm.midi_io.Mapper.MainController = self
        
        #self.garbage_collector = GarbageCollector()
        #self.garbage_collector.start()
        for cg in self.ChildGroups.itervalues():
            node = getattr(cg, 'osc_node', None)
            if node is None:
                continue
            node.send_interrupt_recursive = True
            
    @property
    def session_name(self):
        return self.GLOBAL_CONFIG.get('session_name')
    @session_name.setter
    def session_name(self, value):
        if not self.init_globals:
            self.GLOBAL_CONFIG['session_name'] = value
            
    def on_osc_new_master(self, **kwargs):
        if self.osc_node.oscMaster:
            return
        if self.shutting_down:
            return
        self.request_osc_updates()
        
    def request_osc_updates(self, **kwargs):
        if self.dump_request_thread is not None:
            self.dump_request_thread.stop(blocking=True)
            self.dump_request_thread = None
        self.dump_request_thread = OSCDumpRequest(MainController=self)
        self.dump_request_thread.bind(dump_complete=self.on_dump_request_thread_dump_complete)
        self.dump_request_thread.start()

    def on_dump_request_thread_dump_complete(self, **kwargs):
        self.dump_request_thread = None
        self._clean_groups()
        self._clean_cuestacks()
        for cg in [self.Groups, self.CueStacks]:
            for obj in cg.itervalues():
                obj.request_values_from_osc(to_master=True)
            
    def add_to_selection(self, *args):
        self.Selection.add_devices(args)
        
    def remove_from_selection(self, *args):
        for arg in args:
            self.Selection.del_member(arg.id)
        
    def add_group(self, **kwargs):
        '''
        :Parameters:
            id : 
            name : 
        '''
        gid = kwargs.get('id')
        if gid is not None and gid in self.Groups:
            return self.Groups[gid]
        group = self.Groups.add_child(**kwargs)
        self.emit('new_group', id=group.id, group=group)
        self.emit('child_added')
        return group
        
    def del_group(self, **kwargs):
        gid = kwargs.get('id')
        group = self.Groups.get(gid)
        if group:
            #group.unlink()
            self.Groups.del_child(group)
            #del self.Groups[gid]
            #if group.index in self.groups_indexed:
            #    del self.groups_indexed[group.index]
            ## TODO: make this tell OSC clients to delete the group also
            self.emit('remove_group', id=gid, group=group)
    
    def add_CueStack(self, **kwargs):
        '''
        :Parameters:
            id :
            name :
        '''
        stack = self.CueStacks.add_child(Stack, **kwargs)
        self.emit('new_cuestack', id=stack.id, stack=stack)
        self.emit('child_added')
        return stack
        
    def del_CueStack(self, **kwargs):
        csid = kwargs.get('id')
        cs = self.CueStacks.get(csid)
        if cs:
            cs.unlink()
            self.CueStacks.del_child(cs)
            
        
    def store_to_CueStack(self, **kwargs):
        '''
        :Parameters:
            stack : stack object
            stack_id : stack object id
            cue_index : int, cue number, defaults to stack's current cue
            source : str or list, what to store ('all', 'selected', [list, of, objects])
        '''
        stack = kwargs.get('stack')
        stack_id = kwargs.get('stack_id')
        if not stack and stack_id is not None:
            stack = self.CueStacks[stack_id]
        index = kwargs.get('index', stack.current_index)
        source = kwargs.get('source', 'selected')
        if index:
            if type(source) == list or type(source) == tuple:
                obj_list = source
            elif source == 'selected':
                obj_list = self.Selection.members.values()
            elif source == 'all':
                obj_list = self.AllDevices.members.values()
            else:
                obj_list = []
            objects = dict(zip([obj.id for obj in obj_list], obj_list))
            stack.store_snapshot(index=index, objects=objects)
            return True
        return False
        
    def add_playback(self, **kwargs):
        '''
        :Parameters:
            stack : stack object
            stack_id : stack object id
            name : 
        '''
        stack = kwargs.get('stack')
        stack_id = kwargs.get('stack_id')
        if not stack and stack_id is not None:
            stack = self.CueStacks[stack_id]
        if stack.playback is not None:
            return stack.playback
        pb_kwargs = kwargs.copy()
        pb_kwargs.update(dict(stack=stack, MainController=self))
        pb_kwargs.setdefault('name', stack.name)
        pb_kwargs.setdefault('osc_address', pb_kwargs.get('name'))
        pb = self.Playbacks.add_child(Playback, **pb_kwargs)
        self.emit('playback_added', playback=pb, id=pb.id)
        return pb
    
    def on_ds_new_device(self, **kwargs):
        id = kwargs.get('id')
        if kwargs.get('base') == 'PatchedDevice':
            obj = self.DeviceSystem.PatchedDevices[id]
#            self.AllDevices.membership_lock = False
#            self.AllDevices.add_devices(obj)
#            self.AllDevices.membership_lock = True
            for group in self.Groups.itervalues():
                group.request_member_obj()
            #print 'mc AllDevices:', self.AllDevices.members
            
    def on_group_member_obj_request(self, **kwargs):
        id = kwargs.get('id')
        group = kwargs.get('group')
        d = {}
        m = self.DeviceSystem.PatchedDevices.get(id)
        if not m:
            m = self.Groups.get(id)
        if m:
            d.update({id:m})
            #print 'adding:', id, m
        #else:
        #    print 'id not found:', id
        group.add_member(**d)
        
    def _clean_groups(self):
        for group in self.Groups.itervalues():
            group.request_member_obj()
            group.clean_members()
            
    def _clean_cuestacks(self):
        for cs in self.CueStacks.itervalues():
            cs.reload_member_obj()
            cs.clean_root_members()
        
    def on_osc_request_dump(self, **kwargs):
        if self.osc_node.oscMaster:
            values = kwargs.get('values')
            client = kwargs.get('client')
            #print 'main...client = ', client
            if len(values):
                for i, key in enumerate(values):
                    s = self.to_json(saved_child_objects=[key], indent=None)
                    self.osc_node.send_message(address='dump-response', value=[i, s], client=client)
            else:
                s = self.to_json(indent=None)
                self.osc_node.send_message(address='dump-response', value=[0, s], client=client)
            self.osc_node.send_message(address='dump-response', value='dump-complete', client=client)
        
    def set_interruptors(self, value):
        for cg in self.ChildGroups.itervalues():
            node = getattr(cg, 'osc_node', None)
            if node is None:
                continue
            node.send_interrupt_enabled = value
            
    def load_osc_dump(self, dump):
        self.set_interruptors(True)
        self.loading_from_osc = True
        keys = sorted(dump.keys())
        for key in keys:
            self.from_json(dump[key])
        self.loading_from_osc = False
        self.set_interruptors(False)
        
#    def load_file(self, **kwargs):
#        filename = kwargs.get('filename')
#        self.Archive.load(filename)
#        self._clean_groups()
#        self._clean_cuestacks()
#        if self.current_filename is None:
#            self.current_filename = filename
#        self.emit('child_added')
#        
#    def save_file(self, **kwargs):
#        filename = kwargs.get('filename')
#        self.current_filename = filename
#        self.Archive.save(filename)
        
    def load_show_file(self, **kwargs):
        filename = kwargs.get('filename')
        self.clear_show(reload_globals=False)
        self.Archive.load(filename)
        self._clean_groups()
        self._clean_cuestacks()
        self.emit('child_added')
        
    def save_show_file(self, **kwargs):
        filename = kwargs.get('filename')
        self.Archive.save(filename)
        
    def clear_show(self, reload_globals=True, set_interrupts=False):
        if set_interrupts:
            self.set_interruptors(True)
        self.EffectsSystem.clear_show(set_interrupts)
        for key in self.CueStacks.keys()[:]:
            self.del_CueStack(id=key)
        for key in self.Groups.keys()[:]:
            self.del_group(id=key)
        self.DeviceSystem.clear_show(set_interrupts)
        if reload_globals:
            self.load_globals()
        if set_interrupts:
            self.set_interruptors(False)
    
    def load_globals_file(self, **kwargs):
        self.init_globals = True
        filename = kwargs.get('filename')
        string = ''
        if filename is not None and os.path.exists(filename):
            file = open(filename, 'r')
            for line in file:
                string += line
            file.close()
        if string != '':
            d = Serialization.from_json(string)
            for key in ['DeviceSystem', 'EffectsSystem']:
                if key in d:
                    getattr(self, key).load_globals(string=Serialization.to_json(d[key]))
            self._load_saved_attr(d, saved_child_objects=[])
        self.init_globals = False
        
    def save_globals_file(self, **kwargs):
        filename = kwargs.get('filename')
        d = {}
        for key in ['DeviceSystem', 'EffectsSystem']:
            d.update({key:getattr(self, key).save_globals()})
        file = open(filename, 'w')
        file.write(self.to_json(d, saved_child_objects=[]))
        file.close()
        
    def load_globals(self, **kwargs):
        filename = kwargs.get('filename')
        if filename:
            self.globals_filename = filename
        else:
            filename = self.globals_filename
        self.FileManager.load_file(filename=filename)
        
    def save_globals(self, **kwargs):
        filename = kwargs.get('filename')
        if filename:
            self.globals_filename = filename
        else:
            filename = self.globals_filename
        self.FileManager.save_file(filename=filename)
        
    def on_FileManager_current_file(self, **kwargs):
        old = kwargs.get('old')
        value = kwargs.get('value')
        if value is None and old is not None:
            self.clear_show()
        self.current_filename = kwargs.get('value')
        
#    def load_globals(self, **kwargs):
#        self.init_globals = True
#        kwargs.setdefault('filename', 'Globals.json')
#        filename = kwargs.get('filename')
#        string = kwargs.get('string', '')
#        if filename is not None and os.path.exists(filename):
#            file = open(filename, 'r')
#            for line in file:
#                string += line
#            file.close()
#        if string != '':
#            d = jsonpickle.decode(string)
#            for key in ['DeviceSystem', 'EffectsSystem']:
#                if key in d:
#                    getattr(self, key).load_globals(string=jsonpickle.encode(d[key]))
#            self._load_saved_attr(d, saved_child_objects=[])
#        self.init_globals = False
#        
#    def save_globals(self, **kwargs):
#        filename = kwargs.get('filename', 'Globals.json')
#        d = {}
#        for key in ['DeviceSystem', 'EffectsSystem']:
#            d.update({key:getattr(self, key).save_globals()})
#        file = open(filename, 'w')
#        file.write(self.to_json(d, saved_child_objects=[]))
#        file.close()
        
    def on_app_exit(self, *args, **kwargs):
        #print self.ParentEmissionThread._threaded_calls_queue
        self.stop_ParentEmissionThread()
        self.shutting_down = True
        starttime = time.time()
        self.LOG.info('MainController preparing shutdown. %s current threads: %s' % (threading.active_count(), threading.enumerate()))
        #print threading.enumerate()
        self.EffectsSystem.shutdown()
        self.LOG.info('EffectsSystem shutdown. Time elapsed = ', time.time() - starttime)
        self.DeviceSystem.shutdown()
        self.LOG.info('DeviceSystem shutdown. Time elapsed = ', time.time() - starttime)
        self.comm.shutdown()
        self.LOG.info('Comm shutdown. Time elapsed = ', time.time() - starttime)
        self.LOG.info('%s current threads:' % (threading.enumerate()))
        self.iOsc.unlink()
        #print threading.enumerate()
        
        self.emit('app_exit_complete', starttime=starttime)
    
    def ChildGroup_prepare_child_instance(self, childgroup, cls, **kwargs):
        name = childgroup.name
        kwargs['MainController'] = self
        return cls, kwargs
        
    def _get_saved_attr(self, **kwargs):
        d = super(MainController, self)._get_saved_attr(**kwargs)
        for key in ['AllDevices', 'Selection']:
            id = '_'.join(['MainController', key])
            if id in d['saved_children'].get('Groups', {}):
                del d['saved_children']['Groups'][id]
        return d

class OSCDumpRequest(BaseThread):
    _Events = {'response_complete':{}}
    def __init__(self, **kwargs):
        kwargs['thread_id'] = 'MainController_dump_request'
        super(OSCDumpRequest, self).__init__(**kwargs)
        self.register_signal('dump_complete')
        self.MainController = kwargs.get('MainController')
        self._osc_dumps_pending = {}
        
    def run(self):
        self._running.set()
        mc = self.MainController
        mc.clear_show(reload_globals=False, set_interrupts=True)
        node = mc.osc_node.add_child(name='dump-response')
        node.bind(message_received=self.on_dump_response)
        self.dump_node = node
        mc.osc_node.send_message(address='request-dump', value=list(mc.saved_child_objects), to_master=True)
        self.LOG.info('mc dump requested')
        self.response_complete.wait()
        self.LOG.info('mc dump received')
        if self._running:
            t = mc.DeviceSystem.request_osc_updates()
            self.LOG.info('ds dump requested')
            t.join()
            self.LOG.info('ds dump received')
            t = mc.EffectsSystem.request_osc_updates()
            self.LOG.info('es dump requested')
            t.join()
            self.LOG.info('es dump received')
            self.emit('dump_complete')
            self.stop()
        
    def stop(self, **kwargs):
        blocking = kwargs.get('blocking')
        self.dump_node.unbind(self)
        self._running.clear()
        self.response_complete.set()
        self._stopped.set()
        if blocking:
            self.join()
        
    def on_dump_response(self, **kwargs):
        message = kwargs.get('message')
        values = message.get_arguments()
        if values[0] == 'dump-complete':
            self.MainController.load_osc_dump(self._osc_dumps_pending)
            self.response_complete.set()
            return
        self._osc_dumps_pending[values[0]] = values[1]
